﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Driver.GridFS;

namespace osm2mongo
{
	public class MongoProvider
	{

		private string _mongoHost;

		private string _dbName;

		private MongoClient _client;

		private MongoDatabase _db;
		/// <summary>
		/// The Database object
		/// </summary>
		public MongoDatabase DB
		{
			get
			{
				if (_db == null)
				{
					getDatabase();
				}
				return _db;
			}
		}

		#region MongoProvider Constructor

		/// <summary>
		/// Creates an instance of the MongoDatabase class.
		/// </summary>
		public MongoProvider(string host, string databaseName)
		{
			this._mongoHost = host;
			this._dbName = databaseName;
		}

		#endregion

		#region Get Object

		/// <summary>
		/// Gets an IEnumerable collection of mongo objects, based on a specific query.
		/// </summary>
		/// <typeparam name="T">The type of object to return.</typeparam>
		/// <param name="collectionName">The name of the collection to get the objects from.</param>
		/// <param name="query">The query to get the objects for.</param>
		/// <returns>The IEnumerable collection of objects returned from the query.</returns>
		public MongoCursor<T> GetObjects<T>(string collectionName, IMongoQuery query)
		{

			// Get the collection
			MongoCollection<T> collection = getCollection<T>(collectionName);

			// Get the objects from the collection
			MongoCursor<T> objectsCursor = collection.Find(query);

			// reutrn the cursor object
			return objectsCursor;

		}

		/// <summary>
		/// Gets a single object from the collection, based on the query object.
		/// </summary>
		/// <typeparam name="T">The type of object to return.</typeparam>
		/// <param name="collectionName">The name of the collection to get the object from.</param>
		/// <param name="query">The query to get the object for.</param>
		/// <returns>The object from the database, otherwise null.</returns>
		public T GetObject<T>(string collectionName, IMongoQuery query)
		{

			// Get the collection.
			MongoCollection<T> collection = getCollection<T>(collectionName);

			// Get the object from the database
			return collection.FindOne(query);

		}

		#endregion

		#region Save Object

		/// <summary>
		/// Saves the object to the collection.
		/// </summary>
		/// <typeparam name="T">The type of object to save.</typeparam>
		/// <param name="collectionName">The name of the collection.</param>
		/// <param name="objectToSave">The object to save to the database.</param>
		/// <returns>True if the save was successful, otherwise false.</returns>
		public bool SaveObject<T>(string collectionName, T objectToSave)
		{

			// Get the collection
			MongoCollection<T> collection = getCollection<T>(collectionName);

			// Save the object to the collection
			WriteConcernResult result = collection.Save(objectToSave);

			// Return a yes/no flag.
			return result.Ok;

		}

		#endregion

		#region Map Reduce

		/// <summary>
		/// Executes a MapReduct command on the collection and return the results as an IEnumerable BsonDocument sequence.
		/// </summary>
		/// <param name="collectionName">The name of the collection to execute the command on.</param>
		/// <param name="map">The map function.</param>
		/// <param name="reduce">The reduce function.</param>
		/// <returns>The sequence of results.</returns>
		public IEnumerable<T> MapReduce<T>(string collectionName, BsonJavaScript map, BsonJavaScript reduce)
		{

			// Get the collection
			MongoCollection collection = getCollection(collectionName);

			// Gets the results from the map/reduce operation
			MapReduceResult result = collection.MapReduce(map, reduce);

			// If the result is OK, return the results
			if (result.Ok)
			{
				// Loop through each result and yield it for later use. 
				foreach (T doc in result.GetResultsAs<T>())
				{
					yield return doc;
				}
			}
			else
			{
				// return no items.
				yield break;
			}

		}

		#endregion

		#region Helpers

		/// <summary>
		/// Creates a new instance of the stringly-typed MongoDatabase class.
		/// </summary>
		/// <returns></returns>
		private MongoCollection<T> getCollection<T>(string name)
		{
			
			// Get the mongo collection
			MongoCollection<T> collection = this.DB.GetCollection<T>(name);

			// return the collection
			return collection;

		}

		/// <summary>
		/// Gets an instance of the mongo collection.
		/// </summary>
		/// <param name="name">The name of the collection.</param>
		/// <returns>The collection object.</returns>
		private MongoCollection getCollection(string name)
		{

			// Get the mongo collection
			MongoCollection collection = this.DB.GetCollection(name);

			// return the collection
			return collection;
		}

		/// <summary>
		/// Gets an instance of the MongoDatabase object.
		/// </summary>
		/// <returns>The mongo database object.</returns>
		private void getDatabase()
		{

			string[] hostDetails = this._mongoHost.Split(':');
			string host = hostDetails[0];
			int port;
			Int32.TryParse((hostDetails.Length > 1 ? hostDetails[1] : "27017"), out port);

			// Get the database url
			MongoConnectionStringBuilder builder = new MongoConnectionStringBuilder();
			builder.DatabaseName = this._dbName;
			builder.Server = new MongoServerAddress(hostDetails[0], port);

			// Create the mongo client
			_client = new MongoClient(builder.ConnectionString);

			// Get the database
			_db = _client.GetServer().GetDatabase(builder.DatabaseName);

		}

		#endregion

		#region Grid FS

		/// <summary>
		/// Gets a GridFS File from the database.
		/// </summary>
		/// <param name="id">The ID of the file to return.</param>
		/// <returns>The MongoGridFSFileInfo.</returns>
		public MongoGridFSFileInfo GetGridFile(string id)
		{

			// Get the Mongo GridFS manager
			MongoGridFS gridFS = new MongoGridFS(this.DB);

			QueryDocument query = new QueryDocument("_id", new ObjectId(id));

			// Get the file from the database.
			MongoGridFSFileInfo fileInfo = gridFS.FindOne(query);

			// return the file info
			return fileInfo;

		}

		/// <summary>
		/// Uploads a media file to the database.
		/// </summary>
		/// <param name="fs">The filestream containing the file.</param>
		/// <param name="filename">The filename of the image.</param>
		/// <returns></returns>
		public MongoGridFSFileInfo UploadFile(Stream fs, string filename)
		{

			// Get the Mongo GridFS manager
			MongoGridFS gridFS = new MongoGridFS(this.DB);

			// Upload the file.
			MongoGridFSFileInfo uploadedFile = gridFS.Upload(fs, filename);

			// return the uploaded file
			return uploadedFile;

		}

		#endregion

	}
}
